Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 040a204d274af5b79133f8f233b470c0f9c91fbc


Parents : 6fb0efb
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-18T05:09:30-05:00

feat(scripts): add offline bundle creation and installation scripts

Changes

2 files changed, 235 insertions(+), 0 deletions(-)


Diff

diff --git a/scripts/create-offline-bundle.sh b/scripts/create-offline-bundle.sh
new file mode 100644
index 00000000..5d693b24
--- /dev/null
+++ b/scripts/create-offline-bundle.sh
@@ -0,0 +1,157 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Create an offline bundle for air-gapped (zero-network) builds.
+
+This script downloads and caches everything needed to build MeshChatX without
+any network access. Run it on an online machine, then transfer the resulting
+vendor/offline/ directory to your air-gapped build host.
+
+Usage:
+ bash scripts/create-offline-bundle.sh [options]
+
+Options:
+ --warm-packaging Pre-download electron-builder packaging tools
+ (appimagetool, app-builder, etc.) by running a
+ dummy Linux build. Increases bundle size.
+ --out-dir PATH Output directory (default: vendor/offline)
+ -h, --help Show this help
+
+Prerequisites on the online machine:
+ - pnpm (matching packageManager in package.json)
+ - uv
+ - Node.js >= 24
+ - Python >= 3.11
+
+The bundle includes:
+ - node_modules/ (fully resolved, with all postinstall artifacts)
+ - uv cache (all Python wheels)
+ - electron-builder cache (app-builder and related tools)
+ - Verification that all vendored assets are present
+
+On the air-gapped machine:
+ bash scripts/install-offline.sh
+ MESHCHATX_OFFLINE_BUILD=1 make build
+EOF
+}
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "${ROOT_DIR}"
+
+WARM_PACKAGING="0"
+OUT_DIR="${ROOT_DIR}/vendor/offline"
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --warm-packaging)
+ WARM_PACKAGING="1"
+ shift
+ ;;
+ --out-dir)
+ OUT_DIR="${2:?missing value for --out-dir}"
+ shift 2
+ ;;
+ --)
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+require_cmd() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ echo "Missing required command: $1" >&2
+ exit 1
+ fi
+}
+
+require_cmd node
+require_cmd pnpm
+require_cmd uv
+require_cmd python3
+
+VERSION=$(node -p "require('./package.json').version")
+PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
+ARCH=$(uname -m)
+BUNDLE_NAME="meshchatx-offline-bundle-${VERSION}-${PLATFORM}-${ARCH}"
+BUNDLE_DIR="${OUT_DIR}/${BUNDLE_NAME}"
+
+rm -rf "${BUNDLE_DIR}"
+mkdir -p "${BUNDLE_DIR}"
+
+echo "==> [1/7] Verifying lockfiles..."
+if [[ ! -f "pnpm-lock.yaml" ]]; then
+ echo "Missing pnpm-lock.yaml. Run 'pnpm install' first." >&2
+ exit 1
+fi
+if [[ ! -f "uv.lock" ]]; then
+ echo "Missing uv.lock. Run 'uv lock' first." >&2
+ exit 1
+fi
+
+echo "==> [2/7] Ensuring vendored assets..."
+node scripts/fetch-micron-wasm.mjs
+python3 scripts/build/fetch_reticulum_manual.py
+python3 scripts/build/fetch_repository_wheels.py
+
+echo "==> [3/7] Installing Node.js dependencies..."
+pnpm install --frozen-lockfile
+
+echo "==> [4/7] Running postinstall hooks..."
+mkdir -p "${BUNDLE_DIR}/electron-builder-cache"
+ELECTRON_BUILDER_CACHE="${BUNDLE_DIR}/electron-builder-cache" pnpm run electron-postinstall
+
+echo "==> [5/7] Bundling node_modules..."
+if tar --help 2>/dev/null | grep -q -- '--hard-dereference'; then
+ tar czf "${BUNDLE_DIR}/node_modules.tar.gz" --hard-dereference node_modules/
+else
+ NM_BUNDLE="${BUNDLE_DIR}/node_modules-bundle"
+ cp -rL node_modules "${NM_BUNDLE}"
+ (cd "${BUNDLE_DIR}" && tar czf node_modules.tar.gz node_modules-bundle/)
+ rm -rf "${NM_BUNDLE}"
+fi
+
+echo "==> [6/7] Installing Python dependencies and populating uv cache..."
+mkdir -p "${BUNDLE_DIR}/uv-cache"
+UV_CACHE_DIR="${BUNDLE_DIR}/uv-cache" uv sync --group dev
+
+echo "==> [7/7] Finalizing..."
+if [[ "${WARM_PACKAGING}" == "1" ]]; then
+ echo " Pre-downloading electron-builder packaging tools..."
+ ELECTRON_BUILDER_CACHE="${BUNDLE_DIR}/electron-builder-cache" \
+ npx electron-builder --linux dir --publish=never >/dev/null 2>&1 || true
+fi
+
+cat > "${BUNDLE_DIR}/.bundle-manifest.json" <<EOF
+{
+ "name": "meshchatx-offline-bundle",
+ "version": "${VERSION}",
+ "platform": "${PLATFORM}",
+ "arch": "${ARCH}",
+ "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
+ "warm_packaging": ${WARM_PACKAGING}
+}
+EOF
+
+echo ""
+echo "Offline bundle created: ${BUNDLE_DIR}"
+echo ""
+ls -lh "${BUNDLE_DIR}"
+echo ""
+echo "To transfer to an air-gapped machine:"
+echo " tar czf meshchatx-offline-${PLATFORM}-${ARCH}.tar.gz -C ${OUT_DIR} ${BUNDLE_NAME}"
+echo ""
+echo "On the air-gapped machine:"
+echo " tar xzf meshchatx-offline-${PLATFORM}-${ARCH}.tar.gz"
+echo " bash scripts/install-offline.sh"
+echo " MESHCHATX_OFFLINE_BUILD=1 make build"

diff --git a/scripts/install-offline.sh b/scripts/install-offline.sh
new file mode 100644
index 00000000..2d4b4a83
--- /dev/null
+++ b/scripts/install-offline.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Install MeshChatX dependencies from an offline bundle for air-gapped builds.
+
+Run this on the air-gapped machine after extracting the offline bundle into
+vendor/offline/ (or the project root).
+
+Usage:
+ bash scripts/install-offline.sh
+
+This script:
+ 1. Finds the offline bundle directory
+ 2. Extracts node_modules/ from the bundle (if not already present)
+ 3. Exports cache paths for uv and electron-builder
+ 4. Runs 'make install' in offline mode
+
+After install, build with:
+ MESHCHATX_OFFLINE_BUILD=1 make build
+EOF
+}
+
+if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ usage
+ exit 0
+fi
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "${ROOT_DIR}"
+
+BUNDLE=""
+if [[ -d "${ROOT_DIR}/vendor/offline" ]]; then
+ BUNDLE=$(find "${ROOT_DIR}/vendor/offline" -maxdepth 1 -type d -name 'meshchatx-offline-bundle-*' 2>/dev/null | head -n 1)
+fi
+
+if [[ -z "${BUNDLE}" ]]; then
+ echo "No offline bundle found in ${ROOT_DIR}/vendor/offline/" >&2
+ echo "Create one on an online machine with:" >&2
+ echo " bash scripts/create-offline-bundle.sh" >&2
+ exit 1
+fi
+
+BUNDLE_NAME=$(basename "${BUNDLE}")
+echo "Found offline bundle: ${BUNDLE_NAME}"
+
+if [[ -d "node_modules" ]]; then
+ echo "node_modules already exists. Skipping extraction."
+else
+ NM_TAR="${BUNDLE}/node_modules.tar.gz"
+ if [[ ! -f "${NM_TAR}" ]]; then
+ echo "Missing node_modules.tar.gz in bundle" >&2
+ exit 1
+ fi
+ echo "Extracting node_modules from bundle..."
+ tar xzf "${NM_TAR}"
+ echo "node_modules extracted."
+fi
+
+export MESHCHATX_OFFLINE_BUILD=1
+export UV_CACHE_DIR="${BUNDLE}/uv-cache"
+export ELECTRON_BUILDER_CACHE="${BUNDLE}/electron-builder-cache"
+
+echo ""
+echo "Running offline install..."
+make install
+
+echo ""
+echo "========================================"
+echo "Offline install complete."
+echo ""
+echo "Build with:"
+echo " MESHCHATX_OFFLINE_BUILD=1 make build"
+echo ""
+echo "Or package with:"
+echo " MESHCHATX_OFFLINE_BUILD=1 pnpm run dist:linux"
+echo "========================================"


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────